前面幾天,我們看過了 Ktor 的啟動,路由,以及靜態內容和 HTML 畫面的生成。
今天我們來看看另一個後端框架非常基礎的功能:生成 json 格式的 API 內容。
首先,撰寫之前要先安裝 ContentNegotiation
gradle
要加上
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
然後框架內加上
fun Application.module() {
configureRouting()
configureSerialization()
}
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}
之前看路由時,我們已經看過 install
的實作,我們看看 ContentNegotiation
public val ContentNegotiation: RouteScopedPlugin<ContentNegotiationConfig> = createRouteScopedPlugin(
"ContentNegotiation",
::ContentNegotiationConfig
) {
convertRequestBody()
convertResponseBody()
}
convertRequestBody
實作了轉換請求內容的邏輯,試著將請求的內容轉換成物件
internal fun PluginBuilder<ContentNegotiationConfig>.convertRequestBody() {
onCallReceive { call ->
val registrations = pluginConfig.registrations
val requestedType = call.receiveType
if (requestedType.type in pluginConfig.ignoredTypes) {
LOGGER.trace(
"Skipping for request type ${requestedType.type} because the type is ignored."
)
return@onCallReceive
}
transformBody { body: ByteReadChannel ->
val requestContentType = try {
call.request.contentType().withoutParameters()
} catch (parseFailure: BadContentTypeFormatException) {
throw BadRequestException(
"Illegal Content-Type header format: ${call.request.headers[HttpHeaders.ContentType]}",
parseFailure
)
}
val charset = call.request.contentCharset() ?: Charsets.UTF_8
for (registration in registrations) {
return@transformBody convertBody(body, charset, registration, requestedType, requestContentType)
?: continue
}
LOGGER.trace("No suitable content converter found for request type ${requestedType.type}")
return@transformBody body
}
}
}
如果不是 pluginConfig.ignoredTypes
的話,則會執行 transformBody
public suspend fun transformBody(transform: suspend TransformBodyContext.(body: ByteReadChannel) -> Any) {
val receiveBody = context.subject as? ByteReadChannel ?: return
val typeInfo = context.call.receiveType
if (typeInfo == typeInfo<ByteReadChannel>()) return
val transformContext = TransformBodyContext(typeInfo)
context.subject = transformContext.transform(receiveBody)
}
以這邊的 transform
邏輯來說,會試著用 Charsets.UTF_8
執行 convertBody
private suspend fun convertBody(
body: ByteReadChannel,
charsets: Charset,
registration: ConverterRegistration,
receiveType: TypeInfo,
requestContentType: ContentType
): Any? {
if (!requestContentType.match(registration.contentType)) {
LOGGER.trace(
"Skipping content converter for request type ${receiveType.type} because " +
"content type $requestContentType does not match ${registration.contentType}"
)
return null
}
val converter = registration.converter
val convertedBody = try {
converter.deserialize(charsets, receiveType, body)
} catch (cause: Throwable) {
throw BadRequestException("Failed to convert request body to ${receiveType.type}", cause)
}
return when {
convertedBody != null -> convertedBody
!body.isClosedForRead -> body
receiveType.kotlinType?.isMarkedNullable == true -> NullBody
else -> null
}
}
到這邊,我們可以發現這裡會試著用 Kotlin 的 deserialize
,來將收到的內容轉換成物件。
今天針對 ContentNegotiation
的部分,我們先看到這邊。明天我們來看看 json
的部分是怎麼實作的。